import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { GraphWorkbench } from '@/components/graph/graph-workbench'; import { defaultModeFor, GRAPH_MODES, graphSlugHref, isGraphMode } from '@/components/graph/modes'; import { BreadcrumbLd, Breadcrumbs } from '@/components/meta/breadcrumb-ld'; import { EntityBadge } from '@/components/ui/badges'; import { EntityLink } from '@/components/ui/entity'; import { Container, Note, PageHeader } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { api, ApiError, apiD3, safe } from '@/lib/api'; import { fmtInt } from '@/lib/format'; import { predicateLabel, routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site'; import type { ExploreNode, GraphExploreMode, GraphExplorePayload } from '@/lib/types'; type Params = { params: Promise<{ slug: string }>; searchParams: Promise<{ depth?: string; mode?: string }> }; const LIMIT = 150; const hrefFor = graphSlugHref; /** 404 → notFound(); any other failure → null (the page renders an Unavailable state). */ async function loadGraph(slug: string, mode: GraphExploreMode, depth: 1 | 2): Promise { try { return await apiD3.graphExplore(slug, mode, depth, LIMIT); } catch (e) { if (e instanceof ApiError && e.notFound) notFound(); return null; } } export async function generateMetadata({ params, searchParams }: Params): Promise { const { slug } = await params; const sp = await searchParams; const d = await safe(api.entity(slug)); if (!d) return { title: 'Graph', robots: { index: false } }; const mode = isGraphMode(sp.mode) ? sp.mode : defaultModeFor(d.entity_type); const depth: 1 | 2 = sp.depth === '2' ? 2 : 1; const modeLabel = GRAPH_MODES.find((x) => x.mode === mode)?.label ?? 'Graph'; const title = `${d.name} — ${modeLabel.toLowerCase()} graph`; const og = `${SITE_URL}/graph/og?node=${encodeURIComponent(d.slug)}&mode=${mode}&depth=${depth}`; return { title, description: `Everything AI Atlas links to ${d.name} (${typeLabel(d.entity_type).toLowerCase()}) in ${modeLabel.toLowerCase()} mode, with the predicate of each relation.`, alternates: { canonical: hrefFor(d.slug, mode, depth) }, openGraph: { title: `${title} | ${SITE_NAME}`, type: 'article', images: [{ url: og, width: 1200, height: 630 }] }, twitter: { card: 'summary_large_image', images: [og] } }; } export default async function GraphPage({ params, searchParams }: Params) { const { slug } = await params; const sp = await searchParams; const d = await safe(api.entity(slug)); if (!d) { // distinguish "unknown slug" (404) from "API down" try { await api.entity(slug); } catch (e) { if (e instanceof ApiError && e.notFound) notFound(); } } const mode = isGraphMode(sp.mode) ? sp.mode : defaultModeFor(d?.entity_type); const depth: 1 | 2 = sp.depth === '2' ? 2 : 1; const graph = await loadGraph(slug, mode, depth); const rootId = graph?.root ?? d?.id ?? ''; const nodes = graph?.nodes ?? []; const edges = graph?.edges ?? []; const byId = new Map(nodes.map((n) => [n.id, n])); const groups = new Map(); for (const e of edges) { const isOut = e.source === rootId; const isIn = e.target === rootId; if (!isOut && !isIn) continue; const other = byId.get(isOut ? e.target : e.source); if (!other) continue; const key = `${e.predicate}|${isOut ? 'out' : 'in'}`; (groups.get(key) ?? groups.set(key, []).get(key)!).push({ node: other, direction: isOut ? 'out' : 'in' }); } const modeDef = GRAPH_MODES.find((x) => x.mode === mode)!; const crumbs = [{ name: SITE_NAME, href: '/' }, { name: 'Knowledge graph', href: '/graph' }, ...(d ? [{ name: typeLabel(d.entity_type, true), href: routes.listing(d.entity_type) }, { name: d.name, href: routes.entity(d) }] : []), { name: modeDef.label, href: hrefFor(slug, mode, depth) }]; return ( <> Graph explorer {d && }} title={d ? <>{modeDef.label} around {d.name} : `Around ${slug}`} lede={d ? `${modeDef.hint[0]?.toUpperCase()}${modeDef.hint.slice(1)}${depth === 2 ? ' — two hops' : ''}. Click a node to inspect it, double-click to expand, drag to pan, wheel to zoom.` : undefined} aside={ graph ? (

{fmtInt(graph.counts?.nodes ?? nodes.length)} nodes · {fmtInt(graph.counts?.edges ?? edges.length)} edges{graph.truncated ? · truncated at {LIMIT} : ''}

) : undefined } className="pb-3" >
    {GRAPH_MODES.map((m) => (
  • {m.label}
  • ))}
{!graph || !d ? ( ) : nodes.length <= 1 || edges.length === 0 ? ( Relations are written only when a source states them. Try another mode above, or go back to {d.name} → ) : ( <>

Direct relations of {d.name}, as a list

{groups.size === 0 ? (

No direct relations for the root node in this graph.

) : (
{[...groups.entries()].map(([key, items]) => { const [pred, dir] = key.split('|') as [string, 'out' | 'in']; return (
{predicateLabel(pred, dir)} {fmtInt(items.length)}
{items.map(({ node }) => ( ))}
); })}
)}
{graph.truncated && The neighbourhood is larger than {LIMIT} nodes; the API returned the first {LIMIT} and flagged the cut. Expand individual nodes, or use the entity's Relations block for full lists.}
)}
); }